/*
  Alternative firmware for Itead Sonoff switches, based on the MQTT protocol and a TLS connection
  The very initial version of this firmware was a fork from the SonoffBoilerplate (tzapu)
  This firmware can be easily interfaced with Home Assistant, with the MQTT switch
  component: https://home-assistant.io/components/switch.mqtt/
  CloudMQTT (free until 10 connections): https://www.cloudmqtt.com
  Libraries :
    - ESP8266 core for Arduino :  https://github.com/esp8266/Arduino
    - PubSubClient:               https://github.com/knolleary/pubsubclient
    - WiFiManager:                https://github.com/tzapu/WiFiManager
  Sources :
    - File > Examples > ES8266WiFi > WiFiClient
    - File > Examples > PubSubClient > mqtt_auth
    - https://github.com/tzapu/SonoffBoilerplate
    - https://io.adafruit.com/blog/security/2016/07/05/adafruit-io-security-esp8266/
  Schematic:
    - VCC (Sonoff) -> VCC (FTDI)
    - RX  (Sonoff) -> TX  (FTDI)
    - TX  (Sonoff) -> RX  (FTDI)
    - GND (Sonoff) -> GND (FTDI)
  Steps:
    - Upload the firmware
    - Connect to the new Wi-Fi AP and memorize its name
    - Choose your network and enter your MQTT username, password, broker
      IP address and broker port
    - Update your configuration in Home Assistant
  MQTT topics and payload:
    - State:    <Chip_ID>/switch/state      ON/OFF
    - Command:  <Chip_ID>/switch/switch     ON/OFF
  Configuration (Home Assistant) :
    switch:
      platform: mqtt
      name: 'Switch'
      state_topic: 'CBF777/switch/state'
      command_topic: 'CBF777/switch/switch'
      optimistic: false
  Versions:
    - 1.0: Initial version
    - 1.1: Add TLS support
    - 1.2: Add PIR sensor support
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.

*/

#include <ESP8266WiFi.h>    // https://github.com/esp8266/Arduino
#include <WiFiManager.h>    // https://github.com/tzapu/WiFiManager
#include <PubSubClient.h>   // https://github.com/knolleary/pubsubclient/releases/tag/v2.6
#include <Ticker.h>
#include <EEPROM.h>
//#include <ArduinoOTA.h>


// PIR motion sensor support, make sure to connect a PIR motion sensor to the GPIO14
//#define           PIR
#ifdef PIR
const uint8_t     PIR_SENSOR_PIN = 5;
#endif

// Sonoff properties
const uint8_t     BUTTON_PIN = 14;
const uint8_t     RELAY_PIN  = 13;
const uint8_t     LED_PIN    = 2;

#define           DEBUG                       // enable debugging
#define           STRUCT_CHAR_ARRAY_SIZE 24   // size of the arrays for MQTT username, password, etc.

// macros for debugging
#ifdef DEBUG
  #define         DEBUG_PRINT(x)    Serial.print(x)
  #define         DEBUG_PRINTLN(x)  Serial.println(x)
#else
  #define         DEBUG_PRINT(x)
  #define         DEBUG_PRINTLN(x)
#endif



// MQTT
char              MQTT_CLIENT_ID[7]                                 = {0};
char              MQTT_SWITCH_STATE_TOPIC[STRUCT_CHAR_ARRAY_SIZE]   = {0};
char              MQTT_SWITCH_COMMAND_TOPIC[STRUCT_CHAR_ARRAY_SIZE] = {0};
const char*       MQTT_SWITCH_ON_PAYLOAD                            = "ON";
const char*       MQTT_SWITCH_OFF_PAYLOAD                           = "OFF";

char              HOMEASSISTANT_DISC[STRUCT_CHAR_ARRAY_SIZE] = {0};

// Settings for MQTT
typedef struct {
  char            mqttUser[STRUCT_CHAR_ARRAY_SIZE]                  = "";//{0};
  char            mqttPassword[STRUCT_CHAR_ARRAY_SIZE]              = "";//{0};
  char            mqttServer[STRUCT_CHAR_ARRAY_SIZE]                = "";//{0};
  char            mqttPort[6]                                       = "";//{0};
  char            mqttStateTopic[STRUCT_CHAR_ARRAY_SIZE]            = "";
  char            mqttCommandTopic[STRUCT_CHAR_ARRAY_SIZE]          = "";
} Settings;

enum CMD {
  CMD_NOT_DEFINED,
  CMD_PIR_STATE_CHANGED,
  CMD_BUTTON_STATE_CHANGED,
};
volatile uint8_t cmd = CMD_NOT_DEFINED;

uint8_t           relayState                                        = LOW;  // HIGH: closed switch
uint8_t           buttonState                                       = HIGH; // HIGH: opened switch
uint8_t           currentButtonState                                = buttonState;
long              buttonStartPressed                                = 0;
long              buttonDurationPressed                             = 0;
#ifdef PIR
uint8_t           pirState                                          = LOW;
uint8_t           currentPirState                                   = pirState;
#endif

Settings          settings;
Ticker            ticker;
WiFiClient        wifiClient;
PubSubClient      mqttClient(wifiClient);


void publishSwitchState();
///////////////////////////////////////////////////////////////////////////
//   Sonoff switch
///////////////////////////////////////////////////////////////////////////
/*
 Function called to set the state of the relay
 */
void setRelayState() {
  digitalWrite(RELAY_PIN, relayState);
  digitalWrite(LED_PIN, (relayState + 1) % 2);
  publishSwitchState();
}

/*
  Function called to restart the switch
 */
void restart() {
  DEBUG_PRINTLN(F("INFO: Restart..."));
  ESP.reset();
  delay(1000);
}

/*
  Function called to reset the configuration of the switch
 */
void reset() {
  DEBUG_PRINTLN(F("INFO: Reset..."));
  WiFi.disconnect();
  delay(1000);
  ESP.reset();
  delay(1000);
}
///////////////////////////////////////////////////////////////////////////
//   MQTT
///////////////////////////////////////////////////////////////////////////
/*
   Function called when a MQTT message arrived
   @param p_topic   The topic of the MQTT message
   @param p_payload The payload of the MQTT message
   @param p_length  The length of the payload
*/
void callback(char* p_topic, byte* p_payload, unsigned int p_length) {
  // handle the MQTT topic of the received message
  if (String(settings.mqttCommandTopic).equals(p_topic)) {
  //if (String(MQTT_SWITCH_COMMAND_TOPIC).equals(p_topic)) {
    if ((char)p_payload[0] == (char)MQTT_SWITCH_ON_PAYLOAD[0] && (char)p_payload[1] == (char)MQTT_SWITCH_ON_PAYLOAD[1]) {
      if (relayState != HIGH) {
        relayState = HIGH;
        setRelayState();
      }
    } else if ((char)p_payload[0] == (char)MQTT_SWITCH_OFF_PAYLOAD[0] && (char)p_payload[1] == (char)MQTT_SWITCH_OFF_PAYLOAD[1] && (char)p_payload[2] == (char)MQTT_SWITCH_OFF_PAYLOAD[2]) {
      if (relayState != LOW) {
        relayState = LOW;
        setRelayState();
      }
    }
  }
}

/*
  Function called to publish the state of the Sonoff relay
*/
void publishSwitchState() {
  if (relayState == HIGH) {
    if (mqttClient.publish(settings.mqttStateTopic, MQTT_SWITCH_ON_PAYLOAD, true)) {
//    if (mqttClient.publish(MQTT_SWITCH_STATE_TOPIC, MQTT_SWITCH_ON_PAYLOAD, true)) {
      DEBUG_PRINT(F("INFO: MQTT message publish succeeded. Topic: "));
      DEBUG_PRINT(settings.mqttStateTopic);
      //DEBUG_PRINT(MQTT_SWITCH_STATE_TOPIC);
      DEBUG_PRINT(F(". Payload: "));
      DEBUG_PRINTLN(MQTT_SWITCH_ON_PAYLOAD);
    } else {
      DEBUG_PRINTLN(F("ERROR: MQTT message publish failed, either connection lost, or message too large"));
    }
  } else {
    if (mqttClient.publish(settings.mqttStateTopic, MQTT_SWITCH_OFF_PAYLOAD, true)) {
    //if (mqttClient.publish(MQTT_SWITCH_STATE_TOPIC, MQTT_SWITCH_OFF_PAYLOAD, true)) {
      DEBUG_PRINT(F("INFO: MQTT message publish succeeded. Topic: "));
      //DEBUG_PRINT(MQTT_SWITCH_STATE_TOPIC);
      DEBUG_PRINT(settings.mqttStateTopic);
      DEBUG_PRINT(F(". Payload: "));
      DEBUG_PRINTLN(MQTT_SWITCH_OFF_PAYLOAD);
    } else {
      DEBUG_PRINTLN(F("ERROR: MQTT message publish failed, either connection lost, or message too large"));
    }
  }
}

/*
  Function called to connect/reconnect to the MQTT broker
 */
void reconnect() {
  // test if the module has an IP address
  // if not, restart the module
  if (WiFi.status() != WL_CONNECTED) {
    DEBUG_PRINTLN(F("ERROR: The module isn't connected to the internet"));
    restart();
  }

  // try to connect to the MQTT broker
  // if the connection is not possible, it will reset the settings
  uint8_t i = 0;
  while (!mqttClient.connected()) {
    if (mqttClient.connect(MQTT_CLIENT_ID, settings.mqttUser, settings.mqttPassword)) {
      DEBUG_PRINTLN(F("INFO: The client is successfully connected to the MQTT broker"));
      char HOMEASSISTANT_DISC_PAYLOAD[100];
      sprintf(HOMEASSISTANT_DISC_PAYLOAD,"{\"name\": \"io-switch-%06X\", \"command_topic\": \"%s\", \"state_topic\": \"%s\"}",ESP.getChipId(),MQTT_SWITCH_COMMAND_TOPIC,MQTT_SWITCH_STATE_TOPIC);
      mqttClient.publish(HOMEASSISTANT_DISC, HOMEASSISTANT_DISC_PAYLOAD, true);
    } else {
      DEBUG_PRINTLN(F("ERROR: The connection to the MQTT broker failed"));
      DEBUG_PRINT(F("Username: "));
      DEBUG_PRINTLN(settings.mqttUser);
      DEBUG_PRINT(F("Password: "));
      DEBUG_PRINTLN(settings.mqttPassword);
      DEBUG_PRINT(F("Broker: "));
      DEBUG_PRINTLN(settings.mqttServer);
      delay(1000);
      if (i == 3) {
        reset();
      }
      i++;
    }
  }

  if (mqttClient.subscribe(settings.mqttCommandTopic)) {
  //if (mqttClient.subscribe(MQTT_SWITCH_COMMAND_TOPIC)) {
    DEBUG_PRINT(F("INFO: Sending the MQTT subscribe succeeded. Topic: "));
    DEBUG_PRINTLN(settings.mqttCommandTopic);
    //DEBUG_PRINTLN(MQTT_SWITCH_COMMAND_TOPIC);
  } else {
    DEBUG_PRINT(F("ERROR: Sending the MQTT subscribe failed. Topic: "));
    DEBUG_PRINTLN(settings.mqttCommandTopic);
    //DEBUG_PRINTLN(MQTT_SWITCH_COMMAND_TOPIC);
  }
}

///////////////////////////////////////////////////////////////////////////
//   WiFiManager
///////////////////////////////////////////////////////////////////////////
/*
  Function called to toggle the state of the LED
 */
void tick() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

// flag for saving data
bool shouldSaveConfig = false;

// callback notifying us of the need to save config
void saveConfigCallback () {
  shouldSaveConfig = true;
}

void configModeCallback (WiFiManager *myWiFiManager) {
  ticker.attach(0.2, tick);
}

///////////////////////////////////////////////////////////////////////////
//   ISR
///////////////////////////////////////////////////////////////////////////
/*
  Function called when the button is pressed/released
 */
void buttonStateChangedISR() {
  cmd = CMD_BUTTON_STATE_CHANGED;
}

/*
  Function called when the PIR sensor detects the biginning/end of a mouvement
 */
 #ifdef PIR
void pirStateChangedISR() {
  cmd = CMD_PIR_STATE_CHANGED;
}
#endif

///////////////////////////////////////////////////////////////////////////
//   Setup() and loop()
///////////////////////////////////////////////////////////////////////////
void setup() {
#ifdef DEBUG
  Serial.begin(115200);
#endif

  // init the I/O
  pinMode(LED_PIN,    OUTPUT);
  pinMode(RELAY_PIN,  OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  //attachInterrupt(BUTTON_PIN, buttonStateChangedISR, CHANGE);
#ifdef PIR
  pinMode(PIR_SENSOR_PIN, INPUT);
  attachInterrupt(PIR_SENSOR_PIN, pirStateChangedISR, CHANGE);
#endif
  ticker.attach(0.6, tick);

  // get the Chip ID of the switch and use it as the MQTT client ID
  sprintf(MQTT_CLIENT_ID, "IOTTS-%06X", ESP.getChipId());
  DEBUG_PRINT(F("INFO: MQTT client ID/Hostname: "));
  DEBUG_PRINTLN(MQTT_CLIENT_ID);

  // set the state topic: <Chip ID>/switch/state
  sprintf(MQTT_SWITCH_STATE_TOPIC, "switch/%06X/state", ESP.getChipId());
  DEBUG_PRINT(F("INFO: MQTT state topic: "));
  DEBUG_PRINTLN(MQTT_SWITCH_STATE_TOPIC);

  // set the command topic: <Chip ID>/switch/switch
  sprintf(MQTT_SWITCH_COMMAND_TOPIC, "switch/%06X/switch", ESP.getChipId());
  DEBUG_PRINT(F("INFO: MQTT command topic: "));
  DEBUG_PRINTLN(MQTT_SWITCH_COMMAND_TOPIC);
  
  //
  sprintf(HOMEASSISTANT_DISC,"homeassistant/switch/%06X/config", ESP.getChipId());

  // load custom params
  EEPROM.begin(512);
  EEPROM.get(0, settings);
  EEPROM.end();

  WiFiManagerParameter custom_text("<p>MQTT username, password, broker IP address and broker port</p>");
  WiFiManagerParameter custom_mqtt_server("mqtt-server", "MQTT Broker IP", "192.168.230.227", STRUCT_CHAR_ARRAY_SIZE);

  WiFiManagerParameter custom_mqtt_user("mqtt-user", "MQTT User", "", STRUCT_CHAR_ARRAY_SIZE);
  WiFiManagerParameter custom_mqtt_password("mqtt-password", "MQTT Password", "", STRUCT_CHAR_ARRAY_SIZE, "type = \"password\"");
  WiFiManagerParameter custom_mqtt_port("mqtt-port", "MQTT Broker Port", "1883", 6);

  WiFiManagerParameter custom_mqtt_topics("<p>MQTT state and command topics</p>");
  WiFiManagerParameter custom_mqtt_state_topic("mqtt-state-topic", "MQTT State Topic", MQTT_SWITCH_STATE_TOPIC, STRUCT_CHAR_ARRAY_SIZE);
  WiFiManagerParameter custom_mqtt_command_topic("mqtt-command-topic", "MQTT Command Topic", MQTT_SWITCH_COMMAND_TOPIC, STRUCT_CHAR_ARRAY_SIZE);

  WiFiManager wifiManager;

  wifiManager.addParameter(&custom_text);
  wifiManager.addParameter(&custom_mqtt_user);
  wifiManager.addParameter(&custom_mqtt_password);
  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_mqtt_port);

  wifiManager.addParameter(&custom_mqtt_topics);
  wifiManager.addParameter(&custom_mqtt_state_topic);
  wifiManager.addParameter(&custom_mqtt_command_topic);

  wifiManager.setAPCallback(configModeCallback);

  wifiManager.setConfigPortalTimeout(60);

  // set config save notify callback
  wifiManager.setSaveConfigCallback(saveConfigCallback);
  
  if (!wifiManager.autoConnect(MQTT_CLIENT_ID)) {
    reset();
  }

  if (shouldSaveConfig) {
    strcpy(settings.mqttServer,   custom_mqtt_server.getValue());
    strcpy(settings.mqttUser,     custom_mqtt_user.getValue());
    strcpy(settings.mqttPassword, custom_mqtt_password.getValue());
    strcpy(settings.mqttPort,     custom_mqtt_port.getValue());

    strcpy(settings.mqttStateTopic, custom_mqtt_state_topic.getValue());
    strcpy(settings.mqttCommandTopic, custom_mqtt_command_topic.getValue());

    EEPROM.begin(512);
    EEPROM.put(0, settings);
    EEPROM.end();
  }

  // configure MQTT
  mqttClient.setServer(settings.mqttServer, atoi(settings.mqttPort));
  mqttClient.setCallback(callback);

  // connect to the MQTT broker
  reconnect();

  //ArduinoOTA.setHostname(MQTT_CLIENT_ID);
  //ArduinoOTA.begin();

  ticker.detach();

  setRelayState();
  
}


void loop() {
  //ArduinoOTA.handle();

  //yield();
  
#ifdef PIR
    case CMD_PIR_STATE_CHANGED:
      currentPirState = digitalRead(PIR_SENSOR_PIN);
      if (pirState != currentPirState) {
        if (pirState == LOW && currentPirState == HIGH) {
          if (relayState != HIGH) {
            relayState = HIGH; // closed
            setRelayState();
          }
        } else if (pirState == HIGH && currentPirState == LOW) {
          if (relayState != LOW) {
            relayState = LOW; // opened
            setRelayState();
          }
        }
        pirState = currentPirState;
      }
      cmd = CMD_NOT_DEFINED;
      break;
#endif
      currentButtonState = digitalRead(BUTTON_PIN);
      if (buttonState != currentButtonState) {
        // tests if the button is released or pressed
        if (buttonState == LOW && currentButtonState == HIGH) {
          buttonDurationPressed = millis() - buttonStartPressed;
          if (buttonDurationPressed < 500) {
            relayState = relayState == HIGH ? LOW : HIGH;
            setRelayState();
          } else if (buttonDurationPressed < 3000) {
            restart();
          } else {
            reset();
          }
        } else if (buttonState == HIGH && currentButtonState == LOW) {
          buttonStartPressed = millis();
        }
        buttonState = currentButtonState;
      }

  yield();

  // keep the MQTT client connected to the broker
  if (!mqttClient.connected()) {
    reconnect();
  }
  mqttClient.loop();

  yield();
}
